1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package com.google.common.net;
18
19 import static com.google.common.base.CharMatcher.ASCII;
20 import static com.google.common.base.CharMatcher.JAVA_ISO_CONTROL;
21 import static com.google.common.base.Charsets.UTF_8;
22 import static com.google.common.base.Preconditions.checkArgument;
23 import static com.google.common.base.Preconditions.checkNotNull;
24 import static com.google.common.base.Preconditions.checkState;
25
26 import com.google.common.annotations.Beta;
27 import com.google.common.annotations.GwtCompatible;
28 import com.google.common.base.Ascii;
29 import com.google.common.base.CharMatcher;
30 import com.google.common.base.Function;
31 import com.google.common.base.Joiner;
32 import com.google.common.base.Joiner.MapJoiner;
33 import com.google.common.base.MoreObjects;
34 import com.google.common.base.Objects;
35 import com.google.common.base.Optional;
36 import com.google.common.collect.ImmutableListMultimap;
37 import com.google.common.collect.ImmutableMultiset;
38 import com.google.common.collect.ImmutableSet;
39 import com.google.common.collect.Iterables;
40 import com.google.common.collect.Maps;
41 import com.google.common.collect.Multimap;
42 import com.google.common.collect.Multimaps;
43
44 import java.nio.charset.Charset;
45 import java.nio.charset.IllegalCharsetNameException;
46 import java.nio.charset.UnsupportedCharsetException;
47 import java.util.Collection;
48 import java.util.Map;
49 import java.util.Map.Entry;
50
51 import javax.annotation.Nullable;
52 import javax.annotation.concurrent.Immutable;
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80 @Beta
81 @GwtCompatible
82 @Immutable
83 public final class MediaType {
84 private static final String CHARSET_ATTRIBUTE = "charset";
85 private static final ImmutableListMultimap<String, String> UTF_8_CONSTANT_PARAMETERS =
86 ImmutableListMultimap.of(CHARSET_ATTRIBUTE, Ascii.toLowerCase(UTF_8.name()));
87
88
89 private static final CharMatcher TOKEN_MATCHER = ASCII.and(JAVA_ISO_CONTROL.negate())
90 .and(CharMatcher.isNot(' '))
91 .and(CharMatcher.noneOf("()<>@,;:\\\"/[]?="));
92 private static final CharMatcher QUOTED_TEXT_MATCHER = ASCII
93 .and(CharMatcher.noneOf("\"\\\r"));
94
95
96
97
98 private static final CharMatcher LINEAR_WHITE_SPACE = CharMatcher.anyOf(" \t\r\n");
99
100
101 private static final String APPLICATION_TYPE = "application";
102 private static final String AUDIO_TYPE = "audio";
103 private static final String IMAGE_TYPE = "image";
104 private static final String TEXT_TYPE = "text";
105 private static final String VIDEO_TYPE = "video";
106
107 private static final String WILDCARD = "*";
108
109 private static final Map<MediaType, MediaType> KNOWN_TYPES = Maps.newHashMap();
110
111 private static MediaType createConstant(String type, String subtype) {
112 return addKnownType(new MediaType(type, subtype, ImmutableListMultimap.<String, String>of()));
113 }
114
115 private static MediaType createConstantUtf8(String type, String subtype) {
116 return addKnownType(new MediaType(type, subtype, UTF_8_CONSTANT_PARAMETERS));
117 }
118
119 private static MediaType addKnownType(MediaType mediaType) {
120 KNOWN_TYPES.put(mediaType, mediaType);
121 return mediaType;
122 }
123
124
125
126
127
128
129
130
131
132
133
134 public static final MediaType ANY_TYPE = createConstant(WILDCARD, WILDCARD);
135 public static final MediaType ANY_TEXT_TYPE = createConstant(TEXT_TYPE, WILDCARD);
136 public static final MediaType ANY_IMAGE_TYPE = createConstant(IMAGE_TYPE, WILDCARD);
137 public static final MediaType ANY_AUDIO_TYPE = createConstant(AUDIO_TYPE, WILDCARD);
138 public static final MediaType ANY_VIDEO_TYPE = createConstant(VIDEO_TYPE, WILDCARD);
139 public static final MediaType ANY_APPLICATION_TYPE = createConstant(APPLICATION_TYPE, WILDCARD);
140
141
142 public static final MediaType CACHE_MANIFEST_UTF_8 =
143 createConstantUtf8(TEXT_TYPE, "cache-manifest");
144 public static final MediaType CSS_UTF_8 = createConstantUtf8(TEXT_TYPE, "css");
145 public static final MediaType CSV_UTF_8 = createConstantUtf8(TEXT_TYPE, "csv");
146 public static final MediaType HTML_UTF_8 = createConstantUtf8(TEXT_TYPE, "html");
147 public static final MediaType I_CALENDAR_UTF_8 = createConstantUtf8(TEXT_TYPE, "calendar");
148 public static final MediaType PLAIN_TEXT_UTF_8 = createConstantUtf8(TEXT_TYPE, "plain");
149
150
151
152
153
154 public static final MediaType TEXT_JAVASCRIPT_UTF_8 = createConstantUtf8(TEXT_TYPE, "javascript");
155
156
157
158
159
160
161 public static final MediaType TSV_UTF_8 = createConstantUtf8(TEXT_TYPE, "tab-separated-values");
162 public static final MediaType VCARD_UTF_8 = createConstantUtf8(TEXT_TYPE, "vcard");
163 public static final MediaType WML_UTF_8 = createConstantUtf8(TEXT_TYPE, "vnd.wap.wml");
164
165
166
167
168
169 public static final MediaType XML_UTF_8 = createConstantUtf8(TEXT_TYPE, "xml");
170
171
172 public static final MediaType BMP = createConstant(IMAGE_TYPE, "bmp");
173
174
175
176
177
178
179
180
181
182 public static final MediaType CRW = createConstant(IMAGE_TYPE, "x-canon-crw");
183 public static final MediaType GIF = createConstant(IMAGE_TYPE, "gif");
184 public static final MediaType ICO = createConstant(IMAGE_TYPE, "vnd.microsoft.icon");
185 public static final MediaType JPEG = createConstant(IMAGE_TYPE, "jpeg");
186 public static final MediaType PNG = createConstant(IMAGE_TYPE, "png");
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203 public static final MediaType PSD = createConstant(IMAGE_TYPE, "vnd.adobe.photoshop");
204 public static final MediaType SVG_UTF_8 = createConstantUtf8(IMAGE_TYPE, "svg+xml");
205 public static final MediaType TIFF = createConstant(IMAGE_TYPE, "tiff");
206 public static final MediaType WEBP = createConstant(IMAGE_TYPE, "webp");
207
208
209 public static final MediaType MP4_AUDIO = createConstant(AUDIO_TYPE, "mp4");
210 public static final MediaType MPEG_AUDIO = createConstant(AUDIO_TYPE, "mpeg");
211 public static final MediaType OGG_AUDIO = createConstant(AUDIO_TYPE, "ogg");
212 public static final MediaType WEBM_AUDIO = createConstant(AUDIO_TYPE, "webm");
213
214
215 public static final MediaType MP4_VIDEO = createConstant(VIDEO_TYPE, "mp4");
216 public static final MediaType MPEG_VIDEO = createConstant(VIDEO_TYPE, "mpeg");
217 public static final MediaType OGG_VIDEO = createConstant(VIDEO_TYPE, "ogg");
218 public static final MediaType QUICKTIME = createConstant(VIDEO_TYPE, "quicktime");
219 public static final MediaType WEBM_VIDEO = createConstant(VIDEO_TYPE, "webm");
220 public static final MediaType WMV = createConstant(VIDEO_TYPE, "x-ms-wmv");
221
222
223
224
225
226
227
228 public static final MediaType APPLICATION_XML_UTF_8 = createConstantUtf8(APPLICATION_TYPE, "xml");
229 public static final MediaType ATOM_UTF_8 = createConstantUtf8(APPLICATION_TYPE, "atom+xml");
230 public static final MediaType BZIP2 = createConstant(APPLICATION_TYPE, "x-bzip2");
231
232
233
234
235
236
237
238
239
240 public static final MediaType EOT = createConstant(APPLICATION_TYPE, "vnd.ms-fontobject");
241
242
243
244
245
246
247
248
249
250 public static final MediaType EPUB = createConstant(APPLICATION_TYPE, "epub+zip");
251 public static final MediaType FORM_DATA = createConstant(APPLICATION_TYPE,
252 "x-www-form-urlencoded");
253
254
255
256
257
258
259
260 public static final MediaType KEY_ARCHIVE = createConstant(APPLICATION_TYPE, "pkcs12");
261
262
263
264
265
266
267
268
269
270
271
272 public static final MediaType APPLICATION_BINARY = createConstant(APPLICATION_TYPE, "binary");
273 public static final MediaType GZIP = createConstant(APPLICATION_TYPE, "x-gzip");
274
275
276
277
278
279 public static final MediaType JAVASCRIPT_UTF_8 =
280 createConstantUtf8(APPLICATION_TYPE, "javascript");
281 public static final MediaType JSON_UTF_8 = createConstantUtf8(APPLICATION_TYPE, "json");
282 public static final MediaType KML = createConstant(APPLICATION_TYPE, "vnd.google-earth.kml+xml");
283 public static final MediaType KMZ = createConstant(APPLICATION_TYPE, "vnd.google-earth.kmz");
284 public static final MediaType MBOX = createConstant(APPLICATION_TYPE, "mbox");
285
286
287
288
289
290
291
292 public static final MediaType APPLE_MOBILE_CONFIG =
293 createConstant(APPLICATION_TYPE, "x-apple-aspen-config");
294 public static final MediaType MICROSOFT_EXCEL = createConstant(APPLICATION_TYPE, "vnd.ms-excel");
295 public static final MediaType MICROSOFT_POWERPOINT =
296 createConstant(APPLICATION_TYPE, "vnd.ms-powerpoint");
297 public static final MediaType MICROSOFT_WORD = createConstant(APPLICATION_TYPE, "msword");
298 public static final MediaType OCTET_STREAM = createConstant(APPLICATION_TYPE, "octet-stream");
299 public static final MediaType OGG_CONTAINER = createConstant(APPLICATION_TYPE, "ogg");
300 public static final MediaType OOXML_DOCUMENT = createConstant(APPLICATION_TYPE,
301 "vnd.openxmlformats-officedocument.wordprocessingml.document");
302 public static final MediaType OOXML_PRESENTATION = createConstant(APPLICATION_TYPE,
303 "vnd.openxmlformats-officedocument.presentationml.presentation");
304 public static final MediaType OOXML_SHEET =
305 createConstant(APPLICATION_TYPE, "vnd.openxmlformats-officedocument.spreadsheetml.sheet");
306 public static final MediaType OPENDOCUMENT_GRAPHICS =
307 createConstant(APPLICATION_TYPE, "vnd.oasis.opendocument.graphics");
308 public static final MediaType OPENDOCUMENT_PRESENTATION =
309 createConstant(APPLICATION_TYPE, "vnd.oasis.opendocument.presentation");
310 public static final MediaType OPENDOCUMENT_SPREADSHEET =
311 createConstant(APPLICATION_TYPE, "vnd.oasis.opendocument.spreadsheet");
312 public static final MediaType OPENDOCUMENT_TEXT =
313 createConstant(APPLICATION_TYPE, "vnd.oasis.opendocument.text");
314 public static final MediaType PDF = createConstant(APPLICATION_TYPE, "pdf");
315 public static final MediaType POSTSCRIPT = createConstant(APPLICATION_TYPE, "postscript");
316
317
318
319
320
321 public static final MediaType PROTOBUF = createConstant(APPLICATION_TYPE, "protobuf");
322 public static final MediaType RDF_XML_UTF_8 = createConstantUtf8(APPLICATION_TYPE, "rdf+xml");
323 public static final MediaType RTF_UTF_8 = createConstantUtf8(APPLICATION_TYPE, "rtf");
324
325
326
327
328
329
330
331
332
333 public static final MediaType SFNT = createConstant(APPLICATION_TYPE, "font-sfnt");
334 public static final MediaType SHOCKWAVE_FLASH = createConstant(APPLICATION_TYPE,
335 "x-shockwave-flash");
336 public static final MediaType SKETCHUP = createConstant(APPLICATION_TYPE, "vnd.sketchup.skp");
337 public static final MediaType TAR = createConstant(APPLICATION_TYPE, "x-tar");
338
339
340
341
342
343
344
345
346
347 public static final MediaType WOFF = createConstant(APPLICATION_TYPE, "font-woff");
348 public static final MediaType XHTML_UTF_8 = createConstantUtf8(APPLICATION_TYPE, "xhtml+xml");
349
350
351
352
353
354
355
356 public static final MediaType XRD_UTF_8 = createConstantUtf8(APPLICATION_TYPE, "xrd+xml");
357 public static final MediaType ZIP = createConstant(APPLICATION_TYPE, "zip");
358
359 private final String type;
360 private final String subtype;
361 private final ImmutableListMultimap<String, String> parameters;
362
363 private MediaType(String type, String subtype,
364 ImmutableListMultimap<String, String> parameters) {
365 this.type = type;
366 this.subtype = subtype;
367 this.parameters = parameters;
368 }
369
370
371 public String type() {
372 return type;
373 }
374
375
376 public String subtype() {
377 return subtype;
378 }
379
380
381 public ImmutableListMultimap<String, String> parameters() {
382 return parameters;
383 }
384
385 private Map<String, ImmutableMultiset<String>> parametersAsMap() {
386 return Maps.transformValues(parameters.asMap(),
387 new Function<Collection<String>, ImmutableMultiset<String>>() {
388 @Override public ImmutableMultiset<String> apply(Collection<String> input) {
389 return ImmutableMultiset.copyOf(input);
390 }
391 });
392 }
393
394
395
396
397
398
399
400
401
402 public Optional<Charset> charset() {
403 ImmutableSet<String> charsetValues = ImmutableSet.copyOf(parameters.get(CHARSET_ATTRIBUTE));
404 switch (charsetValues.size()) {
405 case 0:
406 return Optional.absent();
407 case 1:
408 return Optional.of(Charset.forName(Iterables.getOnlyElement(charsetValues)));
409 default:
410 throw new IllegalStateException("Multiple charset values defined: " + charsetValues);
411 }
412 }
413
414
415
416
417
418 public MediaType withoutParameters() {
419 return parameters.isEmpty() ? this : create(type, subtype);
420 }
421
422
423
424
425
426
427 public MediaType withParameters(Multimap<String, String> parameters) {
428 return create(type, subtype, parameters);
429 }
430
431
432
433
434
435
436
437
438
439 public MediaType withParameter(String attribute, String value) {
440 checkNotNull(attribute);
441 checkNotNull(value);
442 String normalizedAttribute = normalizeToken(attribute);
443 ImmutableListMultimap.Builder<String, String> builder = ImmutableListMultimap.builder();
444 for (Entry<String, String> entry : parameters.entries()) {
445 String key = entry.getKey();
446 if (!normalizedAttribute.equals(key)) {
447 builder.put(key, entry.getValue());
448 }
449 }
450 builder.put(normalizedAttribute, normalizeParameterValue(normalizedAttribute, value));
451 MediaType mediaType = new MediaType(type, subtype, builder.build());
452
453 return MoreObjects.firstNonNull(KNOWN_TYPES.get(mediaType), mediaType);
454 }
455
456
457
458
459
460
461
462
463
464
465 public MediaType withCharset(Charset charset) {
466 checkNotNull(charset);
467 return withParameter(CHARSET_ATTRIBUTE, charset.name());
468 }
469
470
471 public boolean hasWildcard() {
472 return WILDCARD.equals(type) || WILDCARD.equals(subtype);
473 }
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501 public boolean is(MediaType mediaTypeRange) {
502 return (mediaTypeRange.type.equals(WILDCARD) || mediaTypeRange.type.equals(this.type))
503 && (mediaTypeRange.subtype.equals(WILDCARD) || mediaTypeRange.subtype.equals(this.subtype))
504 && this.parameters.entries().containsAll(mediaTypeRange.parameters.entries());
505 }
506
507
508
509
510
511
512
513 public static MediaType create(String type, String subtype) {
514 return create(type, subtype, ImmutableListMultimap.<String, String>of());
515 }
516
517
518
519
520
521
522 static MediaType createApplicationType(String subtype) {
523 return create(APPLICATION_TYPE, subtype);
524 }
525
526
527
528
529
530
531 static MediaType createAudioType(String subtype) {
532 return create(AUDIO_TYPE, subtype);
533 }
534
535
536
537
538
539
540 static MediaType createImageType(String subtype) {
541 return create(IMAGE_TYPE, subtype);
542 }
543
544
545
546
547
548
549 static MediaType createTextType(String subtype) {
550 return create(TEXT_TYPE, subtype);
551 }
552
553
554
555
556
557
558 static MediaType createVideoType(String subtype) {
559 return create(VIDEO_TYPE, subtype);
560 }
561
562 private static MediaType create(String type, String subtype,
563 Multimap<String, String> parameters) {
564 checkNotNull(type);
565 checkNotNull(subtype);
566 checkNotNull(parameters);
567 String normalizedType = normalizeToken(type);
568 String normalizedSubtype = normalizeToken(subtype);
569 checkArgument(!WILDCARD.equals(normalizedType) || WILDCARD.equals(normalizedSubtype),
570 "A wildcard type cannot be used with a non-wildcard subtype");
571 ImmutableListMultimap.Builder<String, String> builder = ImmutableListMultimap.builder();
572 for (Entry<String, String> entry : parameters.entries()) {
573 String attribute = normalizeToken(entry.getKey());
574 builder.put(attribute, normalizeParameterValue(attribute, entry.getValue()));
575 }
576 MediaType mediaType = new MediaType(normalizedType, normalizedSubtype, builder.build());
577
578 return MoreObjects.firstNonNull(KNOWN_TYPES.get(mediaType), mediaType);
579 }
580
581 private static String normalizeToken(String token) {
582 checkArgument(TOKEN_MATCHER.matchesAllOf(token));
583 return Ascii.toLowerCase(token);
584 }
585
586 private static String normalizeParameterValue(String attribute, String value) {
587 return CHARSET_ATTRIBUTE.equals(attribute) ? Ascii.toLowerCase(value) : value;
588 }
589
590
591
592
593
594
595 public static MediaType parse(String input) {
596 checkNotNull(input);
597 Tokenizer tokenizer = new Tokenizer(input);
598 try {
599 String type = tokenizer.consumeToken(TOKEN_MATCHER);
600 tokenizer.consumeCharacter('/');
601 String subtype = tokenizer.consumeToken(TOKEN_MATCHER);
602 ImmutableListMultimap.Builder<String, String> parameters = ImmutableListMultimap.builder();
603 while (tokenizer.hasMore()) {
604 tokenizer.consumeCharacter(';');
605 tokenizer.consumeTokenIfPresent(LINEAR_WHITE_SPACE);
606 String attribute = tokenizer.consumeToken(TOKEN_MATCHER);
607 tokenizer.consumeCharacter('=');
608 final String value;
609 if ('"' == tokenizer.previewChar()) {
610 tokenizer.consumeCharacter('"');
611 StringBuilder valueBuilder = new StringBuilder();
612 while ('"' != tokenizer.previewChar()) {
613 if ('\\' == tokenizer.previewChar()) {
614 tokenizer.consumeCharacter('\\');
615 valueBuilder.append(tokenizer.consumeCharacter(ASCII));
616 } else {
617 valueBuilder.append(tokenizer.consumeToken(QUOTED_TEXT_MATCHER));
618 }
619 }
620 value = valueBuilder.toString();
621 tokenizer.consumeCharacter('"');
622 } else {
623 value = tokenizer.consumeToken(TOKEN_MATCHER);
624 }
625 parameters.put(attribute, value);
626 }
627 return create(type, subtype, parameters.build());
628 } catch (IllegalStateException e) {
629 throw new IllegalArgumentException("Could not parse '" + input + "'", e);
630 }
631 }
632
633 private static final class Tokenizer {
634 final String input;
635 int position = 0;
636
637 Tokenizer(String input) {
638 this.input = input;
639 }
640
641 String consumeTokenIfPresent(CharMatcher matcher) {
642 checkState(hasMore());
643 int startPosition = position;
644 position = matcher.negate().indexIn(input, startPosition);
645 return hasMore() ? input.substring(startPosition, position) : input.substring(startPosition);
646 }
647
648 String consumeToken(CharMatcher matcher) {
649 int startPosition = position;
650 String token = consumeTokenIfPresent(matcher);
651 checkState(position != startPosition);
652 return token;
653 }
654
655 char consumeCharacter(CharMatcher matcher) {
656 checkState(hasMore());
657 char c = previewChar();
658 checkState(matcher.matches(c));
659 position++;
660 return c;
661 }
662
663 char consumeCharacter(char c) {
664 checkState(hasMore());
665 checkState(previewChar() == c);
666 position++;
667 return c;
668 }
669
670 char previewChar() {
671 checkState(hasMore());
672 return input.charAt(position);
673 }
674
675 boolean hasMore() {
676 return (position >= 0) && (position < input.length());
677 }
678 }
679
680 @Override public boolean equals(@Nullable Object obj) {
681 if (obj == this) {
682 return true;
683 } else if (obj instanceof MediaType) {
684 MediaType that = (MediaType) obj;
685 return this.type.equals(that.type)
686 && this.subtype.equals(that.subtype)
687
688 && this.parametersAsMap().equals(that.parametersAsMap());
689 } else {
690 return false;
691 }
692 }
693
694 @Override public int hashCode() {
695 return Objects.hashCode(type, subtype, parametersAsMap());
696 }
697
698 private static final MapJoiner PARAMETER_JOINER = Joiner.on("; ").withKeyValueSeparator("=");
699
700
701
702
703
704 @Override public String toString() {
705 StringBuilder builder = new StringBuilder().append(type).append('/').append(subtype);
706 if (!parameters.isEmpty()) {
707 builder.append("; ");
708 Multimap<String, String> quotedParameters = Multimaps.transformValues(parameters,
709 new Function<String, String>() {
710 @Override public String apply(String value) {
711 return TOKEN_MATCHER.matchesAllOf(value) ? value : escapeAndQuote(value);
712 }
713 });
714 PARAMETER_JOINER.appendTo(builder, quotedParameters.entries());
715 }
716 return builder.toString();
717 }
718
719 private static String escapeAndQuote(String value) {
720 StringBuilder escaped = new StringBuilder(value.length() + 16).append('"');
721 for (char ch : value.toCharArray()) {
722 if (ch == '\r' || ch == '\\' || ch == '"') {
723 escaped.append('\\');
724 }
725 escaped.append(ch);
726 }
727 return escaped.append('"').toString();
728 }
729
730 }